/* 
 * Persistence4J - Simple library for data persistence using java
 * Copyright (c) 2010, Avdhesh yadav.
 * http://www.avdheshyadav.com
 * Contact: avdhesh.yadav@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.avdheshyadav.p4j.jdbc.transfer;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang.Validate;

import com.avdheshyadav.p4j.common.ReflectionUtil;
import com.avdheshyadav.p4j.jdbc.model.Column;
import com.avdheshyadav.p4j.jdbc.model.DTO;
import com.avdheshyadav.p4j.jdbc.model.Entity;
import com.avdheshyadav.p4j.jdbc.model.OneToMany;
import com.avdheshyadav.p4j.jdbc.model.OneToOne;
import com.avdheshyadav.p4j.jdbc.service.GenericDTO;

/**
 * 
 * This class is part of the layer which decouples the Persistence Layers GenericDTo from the Application layer entity objects.
 * Full Table Name means schemaname.tablename and table name means just table name which is defined in every entity class.
 * 
 * @author Avdhesh yadav
 */
public class TransferUtil
{
	public static final int ALLOWED_ASSOCIATION_DEPTH = 1;
	//
	private static Map<String, String> mEOMapping = new HashMap<String, String>();
	//
	private static Map<String, Transformer> mTransformerMap = new HashMap<String, Transformer>();
	
	
	/**
	 * 
	 * @param eoClass Class
	 * @param schemaName String
	 * 
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	public static void registerClass(Class eoClass) throws Exception
	{
		Entity entity = getEntity(eoClass);
		
		String tableName = entity.table();
		if(tableName.isEmpty())
			throw new IllegalAccessException("Table for this Entity is can not be empty");
		tableName = tableName.toLowerCase();
		
		String schemaName = entity.schema();
		schemaName = schemaName.trim();
		if(schemaName.isEmpty())
			throw new IllegalAccessException("schema for the Entity " + eoClass.getName()  + " can not be empty.For DBMS that do not support schemas(e.g MySql)use databaseName as schemaName.");
		
		schemaName = schemaName.toLowerCase();
		String fullTableName = schemaName + "." + tableName;
		if(fullTableName == null || fullTableName.isEmpty() || fullTableName.length() == 1)
		{
			throw new IllegalStateException("Full TableName for Entity: " + eoClass.getName() +" is Null or Empty.Please Chaeck and try again");
		}
		else if(!mEOMapping.containsKey(fullTableName))
		{
			mEOMapping.put(fullTableName, eoClass.getName());
			mTransformerMap.put(eoClass.getName(), new Transformer(fullTableName));
		}
	}
	
	
	/**
	 * 
	 * @param geoList List<GenericEO>
	 * 
	 * @return List<? extends Object>
	 * 
	 * @throws Exception
	 */
	public static List<? extends Object> getEntityObject(List<GenericDTO> geoList)throws Exception
	{
		List<Object> eoList = new ArrayList<Object>();
		for(GenericDTO geo : geoList)
		{
			Object eo = (Object)TransferUtil.getEntityObject(geo);
			eoList.add(eo);
		}
		return eoList;
	}
	
	
	/**
	 * 
	 * @param entityObjects List<Object>
	 * 
	 * @return List<GenericDTO>
	 * 
	 * @throws Exception
	 */
	public static List<DTO> getGenericDTOList(List<? extends Object> entityObjects)throws Exception
	{
		List<DTO> dtoList = new ArrayList<DTO>();
		
		for(Object entityObject : entityObjects)
		{
			GenericDTO dto = (GenericDTO)TransferUtil.getGenericDTO(entityObject);
			dtoList.add(dto);
		}
		return dtoList;
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * 
	 * @return GenericEO
	 * 
	 * @throws Exception
	 */
	public static GenericDTO getGenericDTO(Object entityObject) throws Exception
	{
		validateEntity(entityObject);
		Transformer transformer = mTransformerMap.get(entityObject.getClass().getName());
		Validate.notNull(transformer, entityObject.getClass().getName() + ":Entity is Not registered");
		return transformer.toGenericDTO(entityObject);
	}
	
	
	/**
	 * 
	 * @param genericDTO GenericEO
	 * 
	 * @return Object 
	 * 
	 * @throws Exception
	 */
	public static Object getEntityObject(GenericDTO genericDTO)throws Exception
	{
		String eoClass = mEOMapping.get(genericDTO.getTableName());
		if(eoClass == null)
			throw new Exception("EO Class for Table Name:" + genericDTO.getTableName() +" is not registered");
		
		Transformer transformer = mTransformerMap.get(eoClass);
		Object entityObject = (Object)ReflectionUtil.newInstance(eoClass);
		validateEntity(entityObject);
		return transformer.toEntityObject(genericDTO, entityObject);
	}
	
	
	/**
	 * 
	 * @param eoClassName String
	 * 
	 * @return String
	 */
	@SuppressWarnings("unchecked")
	public static String getFullTableNameFromMappings(String eoClassName)
	{
		Iterator<Entry<String, String>> itr = mEOMapping.entrySet().iterator();
		while(itr.hasNext())
		{
			Entry<String, String> entry = itr.next();
			String fullTableName = entry.getKey();
			String value = entry.getValue();
			if(value.equalsIgnoreCase(eoClassName))
			{
				return fullTableName;
			}
		}
		return null;
	}
	
	
	/**
	 * 
	 * @param eoClass Class
	 * 
	 * @return String
	 * 
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	private static Entity getEntity(Class eoClass)throws Exception
	{
		Entity entity = (Entity)eoClass.getAnnotation(Entity.class);
		if(entity == null)
			throw new Exception("Entity Annotations not decleared for this class:"+eoClass.getName());
		return entity;
	}
	
	
	/**
	 *  
	 * @param objects List<? extends Object>
	 * @param batchSize batchSize
	 * 
	 * @return Map<Integer, List>
	 */
	public static Map<Integer, List> getBatches(List<? extends Object> objects , int batchSize) 
	{
		Map<Integer, List> batchMap = new HashMap<Integer, List>();
		
		int noOfBatches = objects.size() / batchSize;
		int start = 0;
		int end = objects.size() - 1;
		
		for(int i = 0; i < noOfBatches ;i++)
		{
			List<Object> batchList = new ArrayList<Object>();
			start = i * batchSize;
			end = start + batchSize;
			while(start < end)
			{
				Object obj = objects.get(start);
				batchList.add(obj);
				start = start + 1;
			}
			batchMap.put(i, batchList);
		}
		
		// now we get the last batch
		List<Object> lastBatchList = new ArrayList<Object>();
		for(int j = (noOfBatches * batchSize) ; j <= (objects.size() - 1); j++ )
		{
			Object obj = objects.get(j);
			lastBatchList.add(obj);
		}
		if(lastBatchList.size() > 0)
			batchMap.put(noOfBatches, lastBatchList);
		
		return batchMap;
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * 
	 * @return List<List<GenericDTO>>
	 * 
	 * @throws Exception
	 */
	public static List<List<GenericDTO>> getAssociations(Object entityObject)throws Exception
	{
		validateEntity(entityObject);
		Field fields [] = entityObject.getClass().getDeclaredFields();
		List<List<GenericDTO>> associatedObjectList = new ArrayList<List<GenericDTO>>();
		List<GenericDTO> oneToOneObjectList = new ArrayList<GenericDTO>();
		for(Field field : fields)
		{
			field.setAccessible(true);
			if(field.isAnnotationPresent(OneToMany.class))
			{
				OneToMany oneToMany = field.getAnnotation(OneToMany.class);
				if(!oneToMany.isLazy())
				{
					Object onetoManyObject = field.get(entityObject);
					if(onetoManyObject != null)
					{
						Class type = field.getType();
						if(type.getName() == List.class.getName())
						{
							Method method = type.getMethod("listIterator", null);
							Iterator iterator = (Iterator)method.invoke(onetoManyObject, null);
							List<GenericDTO> assiciatedOneToManyObjectList = new ArrayList<GenericDTO>();
							while(iterator.hasNext())
							{
									Object obj = iterator.next();
									GenericDTO genericDTO = TransferUtil.getGenericDTO(obj);
									assiciatedOneToManyObjectList.add(genericDTO);
							}
							
							if(assiciatedOneToManyObjectList.size() > 0)
								associatedObjectList.add(assiciatedOneToManyObjectList);
						}
					}
				}
			}
			else if(field.isAnnotationPresent(OneToOne.class))
			{
				OneToOne oneToOne = field.getAnnotation(OneToOne.class);
				if(!oneToOne.isLazy())
				{
					Object oneToOneObject = field.get(entityObject);
					if(oneToOneObject != null)
					{
						GenericDTO genericDTO = TransferUtil.getGenericDTO(oneToOneObject);
						oneToOneObjectList.add(genericDTO);
					}
				}
			}
		}
		
		// if the onetoObjectList have some objects in the list then add it into the associatedObjectylist
		if(oneToOneObjectList.size() > 0)
			associatedObjectList.add(oneToOneObjectList);
		
		return associatedObjectList;
	}
	
	
	/**
	 * 
	 * @param tableName String
	 * 
	 * @return String
	 */
	public static String getEoClassFromTable(String tableName)
	{
		return mEOMapping.get(tableName);
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * 
	 * @return Object
	 * 
	 * @throws Exception
	 */
	public static Object getPrimaryKeyObject(Object entityObject) throws Exception
	{
		Field fields [] = entityObject.getClass().getDeclaredFields();
		for(Field field : fields)
		{
			field.setAccessible(true);
			if(field.isAnnotationPresent(Column.class))
			{
				Column column = field.getAnnotation(Column.class);
				if(column.isPrimaryKey())
				{
					return field.get(entityObject);
				}
			}
		}
		return null;
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * 
	 * @return List<OneToMany>
	 */
	public static List<OneToMany> getOnetoManyAnnotations(Object entityObject)
	{
		Field fields [] = entityObject.getClass().getDeclaredFields();
		List<OneToMany> oneToManyList = new ArrayList<OneToMany>();
		for(Field field : fields)
		{
			if(field.isAnnotationPresent(OneToMany.class))
			{
				OneToMany oneToMany = field.getAnnotation(OneToMany.class);
				oneToManyList.add(oneToMany);
			}
		}
		return oneToManyList;
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * @param oneToMany OneToMany
	 * 
	 * @return Field
	 */
	public static Field  getField(Object entityObject, OneToMany oneToMany)
	{
		Field fields [] = entityObject.getClass().getDeclaredFields();
		for(Field field : fields)
		{
			field.setAccessible(true);
			if(field.isAnnotationPresent(OneToMany.class))
			{
				OneToMany otm = field.getAnnotation(OneToMany.class);
				if(otm.equals(oneToMany))
					return field;
			}
		}
		return null;
	}
	
	
	/**
	 * 
	 * @param entityObject Object
	 * 
	 * @throws Exception
	 */
	public static void validateEntity(Object entityObject) throws Exception
	{
		Validate.notNull(entityObject, "value object can not be null");
		if(!entityObject.getClass().isAnnotationPresent(Entity.class))
			throw new Exception("EntityObject should have Entity Annotation in it");
	}
}